(function($){
	/*
	 * Main function that creates several instances of Graph class
	 */
	$.fn.fsGraph = function (inSettings){
		var defaultSettings = {
			grid : false,
			gridcolor : "#000",
			gap : 20,
			hints : true,
			zoom : true,
			pointWidth : true,
			hintAnimation : 100,
			xScaleAdjust : 10,
			yScaleAdjust : 15
		};

		var settings = $.extend({}, defaultSettings, inSettings || {});

		if(settings.data.x.length > settings.data.y.length){
			settings.data.x = settings.data.x.slice(0, settings.data.y.length);
		}else if(settings.data.x.length < settings.data.y.length){
			settings.data.y = settings.data.y.slice(0, settings.data.x.length);
		}

		return this.each(function(){
			new Graph($(this), settings);
		});
	}

	/*
	 * Main class that represents the graph
	 */
	Graph = function(jElem, settings){
		/* Add new method to Array class that returns min or max value from the set */
		Array.prototype.maxmin = function(maxormin){
			var maxmin = this[0];
			for (var i = 1; i < this.length; i++){
				if(maxormin == "max"){
					maxmin = maxmin > this[i] ? maxmin : this[i];
				}else if(maxormin == "min"){
					maxmin = maxmin < this[i] ? maxmin : this[i];
				}else{
					maxmin = null;
				}
			}
			return maxmin;
		};

			/* max and min values in sequence of incoming date */
		var max = {x : settings.data.x.maxmin("max"), y : settings.data.y.maxmin("max")},
			min = {x : settings.data.x.maxmin("min"), y : settings.data.y.maxmin("min")},
			/* parameters of canvas */
			width = settings.width == undefined ? jElem.width() : settings.width,
			height = settings.height == undefined ? jElem.height() : settings.height,
			canvas = Raphael(jElem[0], width, height);
			/* range in which we should show graph */
			range = settings.range == undefined ? {x : {begin : min.x, end : max.x}, y : {begin : min.y, end : max.y}} : $.extend(true, {}, settings.range),
			/* the quantity of grid */
			xScale = 10,
			yScale = 10,
			/* absoulute value for one segment of grid */
			xStep = (range.x.end - range.x.begin) / xScale,
			yStep = (range.y.end - range.y.begin) / yScale,
			/* flag to figurate when we can shift the graph */
			ifMouseBtnPressed = false,
			ifMouseOut = false,
			mousePosition = {}, // initial position when button was just pressed
			/* real coordinates for each value on the canvas */
			coordinates = { x : [], y : [] };

		//variable for hint frame
		var hints = {
			popup : null,
			label : null,
			popupAttr : {fill: "#2C53B0", stroke: settings.gridcolor, "stroke-width": 2, "fill-opacity": .7},
			textAttr1 : {stroke: settings.gridcolor, font: '0.7em Helvetica, Arial'},
			textAttr2 : {stroke: settings.gridcolor, font: '0.6em Helvetica, Arial'},
			activeIndex : 0
		}

		var draw = {
			/* 
			 * function that creates grid with certain scale
			 */
			grid : function (canvas){
				var factorX = (settings.data.x.maxmin("max")-settings.data.x.maxmin("min"))/(coordinates.x.maxmin("max") - coordinates.x.maxmin("min"));
				var factorY = (settings.data.y.maxmin("max")-settings.data.y.maxmin("min"))/(coordinates.y.maxmin("max") - coordinates.y.maxmin("min"));
				var startX = settings.data.x.maxmin("min") - (coordinates.x.maxmin("min") - settings.gap)*factorX;
				var startY = settings.data.y.maxmin("min") - (height + settings.gap - coordinates.y.maxmin("max"))*factorY;

				for(var i = 0; i <= xScale; i++){
					var x = i * (width/xScale) + settings.gap;
					var y = settings.gap;
					var path = "M" + x + " " + y +
								"L" + x + " " + (y + height);
					canvas.path(path).attr({"stroke-dasharray" : "--", "stroke-width" : 0.5, "stroke" : settings.gridcolor});
					canvas.text(x, canvas.height - settings.gap + settings.yScaleAdjust, (startX + i*factorX*width/xScale).toFixed(2)).attr({"stroke" : settings.gridcolor});
				}
				for(var i =  0; i <= yScale; i++){
					var x = settings.gap;
					var y = height - i * (height/yScale) + settings.gap;
					var path = "M" + x + " " + y +
										"L" + (x + width) + " " + y;
					canvas.path(path).attr({"stroke-dasharray" : "--", "stroke-width" : 0.5, "stroke" : settings.gridcolor});
					canvas.text(x + settings.xScaleAdjust, y, (startY + i*factorY*height/yScale).toFixed(2)).attr({"stroke" : settings.gridcolor});
				}
				//draw axises
				if(startX < 0){
					var x = settings.gap + Math.abs(startX)/factorX;
					canvas.path("M" + x + " " + settings.gap + "L" + x + " " + (height + settings.gap) ).attr({"stroke-width" : 1, "stroke" : settings.gridcolor});
				}
				if(startY < 0){
					var y = height + settings.gap - Math.abs(startY)/factorY;
					canvas.path("M" + settings.gap + " " + y + "L" + (width + settings.gap) + " " + y ).attr({"stroke-width" : 1, "stroke" : settings.gridcolor});
				}
			},
			
			/* 
			 * function that draws the graph
			 */
			graph : function (canvas){
				if(settings.grid){
					draw.grid(canvas);
				}
				var path;
				for (var i = 0; i < coordinates.x.length; i++){
					if(typeof coordinates.x[i] != 'number' || typeof coordinates.y[i] != 'number'){
						continue;
					}
					path += (i == 0 ? "M" : "L") + coordinates.x[i] + " " + coordinates.y[i] + " ";
				}
				canvas.path(path).attr({"stroke-width" : 2, "stroke" : settings.data.color});
				if(settings.pointWidth != undefined){
					for (var i = 0; i < coordinates.x.length; i++){
						canvas.circle(coordinates.x[i], coordinates.y[i], settings.pointWidth).attr({"stroke-width" : 2, "stroke" : settings.data.color, "fill" : "#2C53B0"}).toFront();
					}
				}
			},

			/* 
			 * function that takes a deal with hints
			 */
			hint : function (index, hide){
				if(!settings.hints){
					return;
				}
				var x = parseInt(coordinates.x[index]),
					y = parseInt(coordinates.y[index]),
					hint1 = settings.data.hint1[index],
					hint2 = settings.data.hint2[index];

				if(!hide){
					hints.label = canvas.set();
					hints.label.push(canvas.text(0, 0, hint1 != undefined ? hint1 : "point" + index).attr(hints.textAttr1));
					hints.label.push(canvas.text(0, 15, hint2 != undefined ? hint2 : "point" + index).attr(hints.textAttr2));
				}
				if(settings.data.hint1 != undefined){
					hints.label[0].attr({text: hint1 != undefined ? hint1 : "point" + index});
				}
				if(settings.data.hint2 != undefined){
					hints.label[1].attr({text: hint2 != undefined ? hint2 : "point" + index});
				}

				var side = "right"
				if (hints.popup != undefined && (x + hints.popup.getBBox().width) > width){
					side = "left";
				}
				var inPopup = canvas.popup(x, y, hints.label, side, hide);

				if(!hide){
					inPopup.attr(hints.popupAttr);
				}
				return inPopup;
			}
		},

		graph = {
			/*
			 * converting graphic values to coordinates
			 */
			initCoordinates : function (){
				for (var i = 0; i < settings.data.x.length; i++){
					coordinates.x[i] = (settings.data.x[i] - range.x.begin) * (width / (range.x.end - range.x.begin)) + settings.gap;
					coordinates.y[i] = height - (settings.data.y[i] - range.y.begin) * (height / (range.y.end - range.y.begin)) + settings.gap;
				}
			},

			/*
			 * function that transform coordinates acordingly with zoom and mouse position
			 */
			zoom : function (direction, event){
				var kX = (1 + direction/xScale);
				var kY = (1 + direction/yScale);
				for(var i = 0; i < coordinates.x.length; i++){
					if(typeof coordinates.x[i] != 'number' || typeof coordinates.y[i] != 'number'){
						continue;
					}
					coordinates.x[i] *= kX;
					coordinates.y[i] *= kY;
				}
				var offsetX = (event.pageX - jElem[0].offsetLeft) * (1 - kX);
				var offsetY = (event.pageY - jElem[0].offsetTop) * (1 - kY);
				graph.shift(offsetX, offsetY);
			},

			/*
			 * function that shifts graph
			 */
			shift : function (offsetX, offsetY){
				for(var i = 0; i < coordinates.x.length; i++){
					if(typeof coordinates.x[i] != 'number' || typeof coordinates.y[i] != 'number'){
						continue;
					}
					coordinates.x[i] += offsetX;
					coordinates.y[i] += offsetY;
				}
			}
		},
		event = {
			onMouseWheel : function(event, delta){
				canvas.clear();
				graph.zoom(delta, event);
				draw.graph(canvas);
				hints.popup = draw.hint(hints.activeIndex);
			},
			onMouseDown : function(event){
				ifMouseBtnPressed = true;
				mousePosition.x = event.pageX - jElem[0].offsetLeft;
				mousePosition.y = event.pageY - jElem[0].offsetTop;
			},
			onMouseUp : function(){
				ifMouseBtnPressed = false;
				hints.popup = draw.hint(hints.activeIndex);
			},
			onMouseLeave : function(event){
				ifMouseOut = true;
			},
			onMouseEnter : function(event){
				ifMouseOut = false;
			},
			onMouseMove : function(event){
				if(!ifMouseOut){
					if(ifMouseBtnPressed){
						canvas.clear();
						graph.shift(event.pageX - jElem[0].offsetLeft - mousePosition.x, event.pageY - jElem[0].offsetTop - mousePosition.y);
						mousePosition.x = event.pageX - jElem[0].offsetLeft;
						mousePosition.y = event.pageY - jElem[0].offsetTop;
						draw.graph(canvas);
					}else{
						var min = width;
						var index = 0;
						for(var i = 0; i < coordinates.x.length; i++){
							var delta = Math.abs(coordinates.x[i] - (event.pageX - jElem[0].offsetLeft));
							if(min > delta){
								min = delta;
								index = i;
							}
						}
						if(index != hints.activeIndex){
							var tmpPopup = draw.hint(index, 1);
							hints.popup.show().stop().animate({ path : tmpPopup.path} , settings.hintAnimation);
							hints.label.show().stop().animateWith(hints.popup, {translation: [tmpPopup.dx, tmpPopup.dy]}, settings.hintAnimation);
							hints.activeIndex = index;
						}
					}
				}
			}
		};

		(function main(){
			width -= 2*settings.gap;
			height -= 2*settings.gap;
			graph.initCoordinates();
			draw.graph(canvas);
			hints.popup = draw.hint(hints.activeIndex);

			/*
			 * Handlers for mouse events
			 */
			if(settings.zoom){
				jElem.bind('mousewheel.fsGraph', event.onMouseWheel)
					.bind('mousedown.fsGraph', event.onMouseDown)
					.bind('mouseleave.fsGraph', event.onMouseLeave)
					.bind('mouseenter.fsGraph', event.onMouseEnter)
					.bind('mousemove.fsGraph', event.onMouseMove);
				$(document).bind('mouseup.fsGraph', event.onMouseUp);
			}
		})();

		function debug(msg){
			$('#debug').append(msg);
		}
	};
})(jQuery);